package jmine.tec.utils.spring;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.Callable;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.util.StringUtils;

import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Guarda uma referência serializável de um bean do spring.
 * 
 * @param <T> classe do bean
 * @author wellington.santos
 */
public class SpringBeanReference<T> implements BeanReference<T> {

    private static final ThreadLocal<SpringBeanLocal> SPRING_BEAN_LOCAL = new ThreadLocal<SpringBeanLocal>() {
        @Override
        protected SpringBeanLocal initialValue() {
            return new SpringBeanLocal();
        };
    };

    /**
     * Nome do bean.
     */
    protected String name;

    /**
     * Instância do bean.
     */
    protected transient T bean;

    /**
     * Classe do bean.
     */
    protected final Class<? extends T> type;

    /**
     * Flag usada para comparar se houve notificações de contexto do spring para saber se talvez a definição dos beans tenham mudado.
     */
    protected transient long beanDefinitionChangedFlag;

    /**
     * Construtor interno.
     * 
     * @param name nome do bean
     * @param bean instância do bean
     * @param type classe do bean
     */
    @SuppressWarnings("unchecked")
    protected SpringBeanReference(String name, T bean, Class<? extends T> type) {
        this.name = name;
        this.bean = bean;
        if (type != null) {
            this.type = type;
        } else if (bean != null) {
            this.type = (Class<? extends T>) bean.getClass();
        } else {
            Class<?> argType = null;
            try {
                argType = ReflectionUtils.getTypeArgumentClass(0, this.getClass(), Object.class);
            } catch (Exception e) {
                // ok, o tipo pode ter sido criado sem especificar o argumento genérico
            }
            this.type = (Class<? extends T>) (argType != null ? argType : Object.class);
        }
    }

    /**
     * Obtêm uma referência do bean através de sua instância.
     * 
     * @param <T> classe do bean
     * @param bean instância do bean
     * @return a referência, ou null caso o bean passado seja nulo
     */
    public static <T> SpringBeanReference<T> forBean(T bean) {
        return bean == null ? null : new SpringBeanReference<T>(null, bean, null);
    }

    /**
     * Obtêm uma referência do bean através de seu nome.
     * 
     * @param <T> classe do bean
     * @param name nome do bean
     * @return a referência, ou null caso o nome passado seja nulo
     */
    public static <T> SpringBeanReference<T> forName(String name) {
        return name == null ? null : new SpringBeanReference<T>(name, null, null);
    }

    /**
     * Obtêm uma referência do bean através de seu tipo.
     * 
     * @param <T> classe do bean
     * @param type classe do bean
     * @return a referência, ou null caso o tipo passado seja nulo
     */
    public static <T> SpringBeanReference<T> forType(Class<T> type) {
        return type == null ? null : new SpringBeanReference<T>(null, null, type);
    }

    /**
     * Obtêm uma referência do bean através de seu nome e tipo.
     * 
     * @param <T> classe do bean
     * @param name nome do bean
     * @param type classe do bean
     * @return a referência, ou null caso o nome e tipo passados sejam ambos nulos
     */
    public static <T> SpringBeanReference<T> forNameAndType(String name, Class<T> type) {
        return name == null && type == null ? null : new SpringBeanReference<T>(name, null, type);
    }

    /**
     * @return o nome do bean
     */
    public final String getBeanName() {
        this.resolveBeanName();
        return this.name;
    }

    /**
     * @return a classe do bean (usada na parametrização ao criar a {@link SpringBeanReference})
     */
    public final Class<?> getBeanType() {
        return this.type;
    }

    /**
     * Força que a instância do bean seja obtido novamente através da {@link #getBeanFactory() beanFactory}.
     */
    public final void reload() {
        this.bean = null;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Lança:
     * <ul>
     * <li>IllegalStateException se não foi possível obter a {@link ListableBeanFactory} do contexto spring;</li>
     * <li>NoSuchBeanDefinitionException se a definição do bean não pode ser localizada pelo nome e/ou tipo;</li>
     * <li>BeanNotOfRequiredTypeException se o bean não é do tipo esperado;</li>
     * <li>BeansException se o bean não pode ser criado por algum outro motivo.</li>
     * </ul>
     * </p>
     * 
     * @return {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public final T get() {
        SpringBeanLocal springBeanLocal = getSpringBeanLocal();
        if (this.bean == null
                || (this.beanDefinitionChangedFlag != 0 && springBeanLocal.beanDefinitionChangedFlag != this.beanDefinitionChangedFlag)) {
            // se a definição talvez mudou devemos obtê-lo novamente da fábrica
            ListableBeanFactory beanFactory = springBeanLocal.hasBeanFactory() ? springBeanLocal.getBeanFactory() : this.getBeanFactory();
            this.resolveBeanName();
            this.beanDefinitionChangedFlag = springBeanLocal.beanDefinitionChangedFlag;
            this.bean = (T) beanFactory.getBean(this.name, this.type);
        }
        return this.bean;
    }

    /**
     * Descobre o nome do bean caso seja nulo.
     */
    protected void resolveBeanName() {
        if (this.name == null) {
            ListableBeanFactory beanFactory = this.getBeanFactory();
            String[] namesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, this.type);
            if (namesForType.length == 1) {
                this.name = namesForType[0];
                return;
            }
            if (namesForType.length > 1) {
                for (String beanName : namesForType) {
                    if (this.type == beanFactory.getType(beanName)) {
                        this.name = beanName;
                        return;
                    }
                }
                if (beanFactory instanceof DefaultListableBeanFactory) {
                    DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) beanFactory;
                    int autowireCandidateCount = 0;
                    String autowireCandidateName = null;
                    for (String beanName : namesForType) {
                        if (defaultBeanFactory.getBeanDefinition(beanName).isAutowireCandidate()) {
                            autowireCandidateName = beanName;
                            autowireCandidateCount++;
                            if (autowireCandidateCount > 1) {
                                break;
                            }
                        }
                    }
                    if (autowireCandidateCount == 1) {
                        this.name = autowireCandidateName;
                        return;
                    }
                }
                throw new NoSuchBeanDefinitionException(this.type, "More than 1 bean for type \"" + this.type + "\" found: "
                        + StringUtils.arrayToCommaDelimitedString(namesForType));
            }
            throw new NoSuchBeanDefinitionException(this.type, "No bean for type " + this.type + " found");
        }
    }

    /**
     * Resolve o nome e tipo do bean antes da serialização.
     * 
     * @param out {@link ObjectOutputStream}
     * @throws IOException caso ocorra um erro de I/O
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        // resolve o nome do bean antes de serializar
        this.resolveBeanName();
        out.defaultWriteObject();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    public String toString() {
        final String typeLabel = "SpringBeanReference<";
        final String nameLabel = "[name = ";
        int capacity = this.type.getName().length() + 1;
        if (this.name != null) {
            capacity += this.name.length() + 1;
        }
        StringBuilder result = new StringBuilder(capacity);
        result.append(typeLabel).append(this.type).append(">");
        if (this.name != null) {
            result.append(nameLabel).append(this.name).append("]");
        }
        return result.toString();
    }

    /**
     * Obtém a {@link ListableBeanFactory} usada para instânciar o {@link #get() bean}. <br>
     * Lança IllegalStateException se não foi possível obter a {@link ListableBeanFactory} do contexto spring.
     * 
     * @return a {@link ListableBeanFactory}
     */
    public ListableBeanFactory getBeanFactory() {
        return getSpringBeanLocal().getBeanFactory();
    }

    /**
     * Define a {@link ListableBeanFactory beanFactory} que será usada para a thread atual.
     * 
     * @param beanFactory {@link ListableBeanFactory}
     * @return a {@link ListableBeanFactory beanFactory} ques estava sendo usada atualmente ou <code>null</code> se nenhuma tinha sido
     * carregada
     */
    public static ListableBeanFactory setThreadLocalBeanFactory(ListableBeanFactory beanFactory) {
        SpringBeanLocal local = getSpringBeanLocal();
        ListableBeanFactory old = local.hasBeanFactory() ? local.getBeanFactory() : null;
        local.setBeanFactory(beanFactory);
        return old;
    }

    /**
     * @return a instância de {@link SpringBeanLocal} para {@link Thread} atual
     */
    protected static SpringBeanLocal getSpringBeanLocal() {
        return SPRING_BEAN_LOCAL.get();
    }

    /**
     * Classe para armazenar os dados em escopo de {@link Thread}.
     * 
     * @author wellington.santos
     */
    protected static class SpringBeanLocal implements ApplicationListener {

        private static final int BITS_PER_BYTE = 8;

        private static final int MASK_OF_8BITS = 0xff;

        // referência fraca para permitir a beanFactory ser coletada pelo GC
        private WeakReference<ListableBeanFactory> beanFactoryReference;

        protected long beanDefinitionChangedFlag;

        /**
         * Construtor.
         */
        public SpringBeanLocal() {
            this.notifyBeanDefinitionChanged();
        }

        /**
         * Define a {@link ListableBeanFactory}.
         * 
         * @param beanFactory {@link ListableBeanFactory}
         */
        public void setBeanFactory(ListableBeanFactory beanFactory) {
            WeakReference<ListableBeanFactory> reference = this.beanFactoryReference;
            ListableBeanFactory oldFactory = reference == null ? null : reference.get();
            if (oldFactory != beanFactory) {
                this.removeListener(oldFactory);
                if (beanFactory == null) {
                    this.beanFactoryReference = null;
                } else {
                    this.beanFactoryReference = new WeakReference<ListableBeanFactory>(beanFactory);
                    this.addListener(beanFactory);
                }
                this.notifyBeanDefinitionChanged();
            }
        }

        /**
         * Configura o listener para escutar quando a definição dos beans mudarem.
         * 
         * @param beanFactory {@link ListableBeanFactory}
         */
        private void addListener(ListableBeanFactory beanFactory) {
            if (beanFactory instanceof ConfigurableApplicationContext) {
                ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) beanFactory;
                // necessário sincronizar pois a lista de listeners do contexto
                // não é thread-safe
                synchronized (ctx) {
                    ctx.addApplicationListener(this);
                }
            }
        }

        /**
         * Configura o listener para escutar quando a definição dos beans mudarem.
         * 
         * @param beanFactory {@link ListableBeanFactory}
         */
        private void removeListener(ListableBeanFactory beanFactory) {
            if (beanFactory instanceof AbstractApplicationContext) {
                AbstractApplicationContext ctx = (AbstractApplicationContext) beanFactory;
                // necessário sincronizar pois a lista de listeners do contexto
                // não é thread-safe
                synchronized (ctx) {
                    ctx.getApplicationListeners().remove(this);
                }
            }
        }

        /**
         * Obtém a {@link ListableBeanFactory}. Lança IllegalStateException se não foi possível obter a {@link ListableBeanFactory} do
         * contexto spring
         * 
         * @return {@link ListableBeanFactory}
         */
        public ListableBeanFactory getBeanFactory() {
            WeakReference<ListableBeanFactory> reference = this.beanFactoryReference;
            ListableBeanFactory beanFactory = reference == null ? null : reference.get();
            if (beanFactory == null) {
                throw new IllegalStateException(
                        "Could not find ListableBeanFactory: missed a call to setThreadLocalBeanFactory(beanFactory) in the servlet filter?");
            }
            return beanFactory;
        }

        /**
         * @return true se a {@link #getBeanFactory() beanFactory} está carregada
         */
        public boolean hasBeanFactory() {
            WeakReference<ListableBeanFactory> reference = this.beanFactoryReference;
            return reference != null && reference.get() != null;
        }

        /**
         * @return flag que indica a última atualização da {@link #getBeanFactory() beanFactory}.
         */
        public final long getBeanDefinitionChangedFlag() {
            return this.beanDefinitionChangedFlag;
        }

        /**
         * Modifica a flag que indicando que houve uma atualização da {@link #getBeanFactory() beanFactory}.
         */
        public final void notifyBeanDefinitionChanged() {
            long oldValue = this.beanDefinitionChangedFlag;
            long newValue = System.nanoTime();
            // Esta flag é composta de uma constante e dois contatores:
            // - O primeiro bit é sempre 1 para que a flag nunca tenha valor 0,
            // necessário para identificar quando um bean foi referenciado usando
            // o método forBean() e neste caso não devemos consultar a beanFactory
            // por mudanças na definição deste objeto pois talvez ele pertença a
            // um contexto em particular ao invés do que nós temos na thread-local;
            // - O os próximos 7 bits aumentam um número incrementalmente para se
            // diferenciar entre chamadas consecutivas ao método onApplicationEvent;
            // - Os outros 7 bytes guardam a hora do temporizador para
            // identificar quando a beanFactory mudar por estarmos olhando a
            // SpringBeanReference de uma thread diferente da que ela foi criada.
            this.beanDefinitionChangedFlag = (newValue << BITS_PER_BYTE) | ((((oldValue >> 1) + 1) << 1) & MASK_OF_8BITS) | 1;
        }

        /**
         * {@inheritDoc}
         */
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ApplicationContextEvent) {
                this.notifyBeanDefinitionChanged();
            }
        }
    }

    /**
     * Classe que faz uma cópia da Thread corrente e usa a mesma para executar o comando implementado.
     * 
     * @author danilo.saita
     * @Jun 15, 2012
     */
    public abstract static class ThreadLocalCopyCallable implements Callable<Void> {

        private final SpringBeanLocal localSpringBean;

        /**
         * Construtor que salva a Thread atual.
         */
        public ThreadLocalCopyCallable() {
            this.localSpringBean = SPRING_BEAN_LOCAL.get();
        }

        /**
         * Copia a Thread corrente e chama o comando implementado em doCall()
         * 
         * @return Void
         * @throws Exception Qualquer tipo de exceção
         */
        public final Void call() throws Exception {
            SPRING_BEAN_LOCAL.set(this.localSpringBean);
            return this.doCall();
        }

        /**
         * @return Void
         * @throws Exception Qualquer tipo de exceção
         */
        protected abstract Void doCall() throws Exception;
    }
}
